using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Globalization;
using System.Threading;

using Yorrick.FlickrMetadataSynchr.Data;
using Yorrick.FlickrMetadataSynchr.Events;
using Yorrick.FlickrMetadataSynchr.Properties;
using System.Windows.Threading;

namespace Yorrick.FlickrMetadataSynchr.Local
{
    /// <summary>
    /// Class that helps with getting and setting of metadata for local picture files.
    /// </summary>
    public class LocalPicturesHelper
    {
        #region Inner class WicStaCommunication
        /// <summary>
        /// Inner class that contains data for cross-thread communication.
        /// </summary>
        private class WicStaCommunication
        {
            public AutoResetEvent Signal = new AutoResetEvent(false);

            public Dispatcher Dispatcher;
        }
        #endregion

        #region Private constants
        /// <summary>
        /// The EXIF metadata query for GPS latitude.
        /// </summary>
        private const string _LatitudeQuery = "/app1/ifd/gps/subifd:{ulong=2}";

        /// <summary>
        /// The EXIF metadata query for GPS longitude.
        /// </summary>
        private const string _LongitudeQuery = "/app1/ifd/gps/subifd:{ulong=4}";

        /// <summary>
        /// The EXIF metadata query for GPS North or South.
        /// </summary>
        private const string _NorthOrSouthQuery = "/app1/ifd/gps/subifd:{uint=1}";

        /// <summary>
        /// The EXIF metadata query for GPS East or West.
        /// </summary>
        private const string _EastOrWestQuery = "/app1/ifd/gps/subifd:{uint=3}";

        /// <summary>
        /// The XMP metadata query for GPS latitude.
        /// </summary>
        private const string _XmpLatitudeQuery = "/xmp/exif:GPSLatitude";

        /// <summary>
        /// The XMP metadata query for the GPS longitude.
        /// </summary>
        private const string _XmpLongitudeQuery = "/xmp/exif:GPSLongitude";

        /// <summary>
        /// The metadata query for the GPS version.
        /// </summary>
        private const string _GpsVersionQuery = "/app1/ifd/gps/";

        /// <summary>
        /// The metadata query for the Flickr id.
        /// </summary>
        private const string _UniqueImageIdQuery = "System.Image.ImageID";

        private const string _AlternateTitleQuery1 = "/xmp/photoshop:Headline";
        private const string _AlternateTitleQuery2 = "/app13/{ushort=0}/{ulonglong=61857348781060}/iptc/{str=Headline}";

        private const string _AlternateDescriptionQuery1 = "/app1/ifd/{ushort=270}";
        private const string _AlternateDescriptionQuery2 = "/xmp/dc:description";
        private const string _AlternateDescriptionQuery3 = "/app13/{ushort=0}/{ulonglong=61857348781060}/iptc/{str=Caption}";

        /// <summary>
        /// The amount of padding to add for metadata.
        /// </summary>
        private const uint _MetadataPaddingInBytes = 2048;

        /// <summary>
        /// The COM Exception error code in case of not enough space to add padding (0x88982f52, WINCODEC_ERR_TOOMUCHMETADATA).
        /// </summary>
        private const uint _PaddingCOMExceptionErrorCode = 0x88982F52;

        #endregion

        #region Private fields
        /// <summary>
        /// Whether or not to simulate updating metadata.
        /// </summary>
        private bool _simulateUpdatingMetadata = false;

        /// <summary>
        /// Instance that is used for cross-thread communication.
        /// </summary>
        private WicStaCommunication _wicStaCommunication;

        /// <summary>
        /// What to do with Flickr id metadata.
        /// </summary>
        private FlickrIdBehavior _flickrIdBehavior = FlickrIdBehavior.AddOrUpdate;
        #endregion

        #region Constructor
        /// <summary>
        /// Constructs an instance.
        /// </summary>
        public LocalPicturesHelper()
        {
            StartWicStaThread();
        }
        #endregion

        #region Public properties
        /// <summary>
        /// Gets or sets whether or not to simulate updating metadata.
        /// </summary>
        public bool SimulateUpdatingMetadata
        {
            get
            {
                return _simulateUpdatingMetadata;
            }
            set
            {
                _simulateUpdatingMetadata = value;
            }
        }

        /// <summary>
        /// Gets or sets what to do with Flickr id metadata.
        /// </summary>
        public FlickrIdBehavior FlickrIdBehavior
        {
            get
            {
                return _flickrIdBehavior;
            }
            set
            {
                _flickrIdBehavior = value;
            }
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Returns a list of local picture files sorted by filename.
        /// </summary>
        /// <param name="localFolder">The folder to return all pictures for.</param>
        /// <param name="includeSubFolders">Whether or not to include pictures in subfolders.</param>
        /// <returns>A sorted list of local picture files.</returns>
        public IList<string> GetPictureFileNames(string localFolder, bool includeSubFolders)
        {
            SearchOption searchOption = includeSubFolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;

            string[] jpgFiles = Directory.GetFiles(localFolder, "*.jpg", searchOption);
            string[] jpegFiles = Directory.GetFiles(localFolder, "*.jpeg", searchOption);

            List<string> files = new List<string>(jpgFiles.Length + jpegFiles.Length);

            files.AddRange(jpgFiles);
            files.AddRange(jpegFiles);
            files.Sort();

            Trace.TraceInformation("Found {0} local pictures.", files.Count);

            return files.AsReadOnly();
        }

        /// <summary>
        /// Returns the metadata to sync for a local picture.
        /// </summary>
        /// <param name="pictureFileName">A filename of a local picture.</param>
        /// <returns>The metadata for the local picture.</returns>
        /// <exception cref="LocalMetadataException">Thrown when the metadata could not be fetched from
        /// the local picture.</exception>
        public LocalPictureMetadata GetSyncMetadata(string pictureFileName)
        {
            try
            {
                using (Stream pictureFileStream = new FileStream(pictureFileName, FileMode.Open, FileAccess.Read))
                {
                    // Disable caching to prevent excessive memory usage.
                    JpegBitmapDecoder decoder = new JpegBitmapDecoder(pictureFileStream, BitmapCreateOptions.None, BitmapCacheOption.None);

                    BitmapMetadata bitmapMetadata = (BitmapMetadata)decoder.Frames[0].Metadata; ;

                    if (bitmapMetadata == null)
                    {
                        return null;
                    }

                    LocalPictureMetadata pictureMetadata = ExtractMetadata(pictureFileName, bitmapMetadata);

                    return pictureMetadata;
                }
            }
            catch (COMException ex)
            {
                throw GetReadLocalMetadataException(pictureFileName, ex);
            }
            catch (NotSupportedException ex)
            {
                throw GetReadLocalMetadataException(pictureFileName, ex);
            }
            catch (InvalidOperationException ex)
            {
                throw GetReadLocalMetadataException(pictureFileName, ex);
            }
            catch (FileFormatException ex)
            {
                throw GetReadLocalMetadataException(pictureFileName, ex);
            }
        }

        /// <summary>
        /// Extracts metadata from the WIC form <paramref name="bitmapMetadata"/> into the format
        /// of the application for the picture with the specified filename.
        /// </summary>
        /// <param name="pictureFileName">The file name of the picture.</param>
        /// <param name="bitmapMetadata">The WIC metadata for the picture file.</param>
        /// <returns>The metadata in the internal format of the application.</returns>
        private static LocalPictureMetadata ExtractMetadata(string pictureFileName, BitmapMetadata bitmapMetadata)
        {
            LocalPictureMetadata pictureMetadata = new LocalPictureMetadata(pictureFileName);

            if (bitmapMetadata.ContainsQuery(_UniqueImageIdQuery))
            {
                string metadata = (string)bitmapMetadata.GetQuery(_UniqueImageIdQuery);
                pictureMetadata.FlickrId = FlickrIdHelper.GetFlickrIdFromMetadata(metadata);
            }

            if (!Settings.Default.UseAlternateLocalMetadataMapping)
            {
                pictureMetadata.Title = FixEmptyMetadata(bitmapMetadata.Title);
                pictureMetadata.Description = FixEmptyMetadata(bitmapMetadata.Comment);
            }
            else
            {
                pictureMetadata.Title = (string) bitmapMetadata.GetQuery(_AlternateTitleQuery1);
                pictureMetadata.Description = (string)bitmapMetadata.GetQuery(_AlternateDescriptionQuery1);
            }

            pictureMetadata.DateTaken = Convert.ToDateTime(bitmapMetadata.DateTaken);
            ReadOnlyCollection<string> authors = bitmapMetadata.Author;
            if (authors != null && authors.Count > 0)
            {
                pictureMetadata.Author = authors[0];
            }

            if (bitmapMetadata.Keywords != null)
            {
                // First set the values in the string array before assigning it to the metadata structure.
                // Otherwise the original values will not be set correctly in the metadata structure.
                string[] tags = new string[bitmapMetadata.Keywords.Count];
                bitmapMetadata.Keywords.CopyTo(tags, 0);
                pictureMetadata.Tags = tags;
            }

            bool didReadGpsData = TryReadGpsData(bitmapMetadata, pictureMetadata);

            if (!didReadGpsData)
            {
                Trace.TraceWarning("Unsupported GPS version for local picture '{0}'.", pictureFileName);
            }

            pictureMetadata.LastUpdated = File.GetLastWriteTimeUtc(pictureFileName);

            pictureMetadata.TrackChanges = true;
            return pictureMetadata;
        }

        /// <summary>
        /// Tries to read the GPS data from the bitmap metadata and stores it in a <see cref="LocalPictureMetadata"/> instance
        /// when successful.
        /// </summary>
        /// <param name="bitmapMetadata">The metadata for the picture file.</param>
        /// <param name="pictureMetadata">The metadata structure in which the GPS info is stored after reading.</param>
        /// <returns><c>true</c> when the GPS data could be read, <c>false</c> otherwise.</returns>
        private static bool TryReadGpsData(BitmapMetadata bitmapMetadata, LocalPictureMetadata pictureMetadata)
        {
            byte[] gpsVersionBytes = bitmapMetadata.GetQuery(_GpsVersionQuery) as byte[];

            GpsVersion gpsVersion = new GpsVersion(gpsVersionBytes);

            if (!gpsVersion.IsSupportedVersion)
            {
                return false;
            }

            pictureMetadata.GpsLatitude = GetLatitude(bitmapMetadata);

            pictureMetadata.GpsLongitude = GetLongitude(bitmapMetadata);

            return true;
        }

        /// <summary>
        /// Reads the GPS latitude from the specified WIC metadata.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC metadata.</param>
        /// <returns>The GPS latitude or <c>null</c> when there is no latitude set.</returns>
        private static double? GetLatitude(BitmapMetadata bitmapMetadata)
        {
            ulong[] latitudes = bitmapMetadata.GetQuery(_LatitudeQuery) as ulong[];

            if (latitudes == null)
            {
                return null;
            }

            double latitude = GpsCoordinatesHelper.DecodeExifCoordinate(latitudes);
            // N or S
            string northOrSouth = (string)bitmapMetadata.GetQuery(_NorthOrSouthQuery);
            if (northOrSouth == "S")
            {
                // South means negative latitude.
                latitude = -latitude;
            }

            return latitude;
        }

        /// <summary>
        /// Reads the GPS longitude from the specified WIC metadata.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC metadata.</param>
        /// <returns>The GPS longitude or <c>null</c> when there is no longitude set.</returns>
        private static double? GetLongitude(BitmapMetadata bitmapMetadata)
        {
            ulong[] longitudes = bitmapMetadata.GetQuery(_LongitudeQuery) as ulong[];

            if (longitudes == null)
            {
                return null;
            }

            double longitude = GpsCoordinatesHelper.DecodeExifCoordinate(longitudes);

            // E or W
            string eastOrWest = (string)bitmapMetadata.GetQuery(_EastOrWestQuery);
            if (eastOrWest == "W")
            {
                // West means negative longitude.
                longitude = -longitude;
            }

            return longitude;
        }

        /// <summary>
        /// Returns the metada to sync for a set of local pictures.
        /// </summary>
        /// <param name="pictureFileNames">A list of file names of local pictures.</param>
        /// <param name="progressCallback">A callback to receive progress information. Is allowed to be null.</param>
        /// <returns>The metadata for the local pictures.</returns>
        public IList<LocalPictureMetadata> GetSyncMetadata(IList<string> pictureFileNames, EventHandler<PictureProgressEventArgs> progressCallback)
        {
            int numberOfPicturesToProcess = pictureFileNames.Count;

            List<LocalPictureMetadata> pictureMetadataList = new List<LocalPictureMetadata>(numberOfPicturesToProcess);

            int currentPictureNumber = 1;
            foreach (string pictureFilename in pictureFileNames)
            {
                AddSyncMetadata(pictureFilename, pictureMetadataList);

                bool cancel = ReportProgressAndCheckCancel(progressCallback, currentPictureNumber, numberOfPicturesToProcess);
                if (cancel)
                {
                    return null;
                }

                currentPictureNumber++;
            }

            return pictureMetadataList.AsReadOnly();
        }

        /// <summary>
        /// Adds the metada for a local picture to the list if it can be read successfully.
        /// </summary>
        /// <param name="pictureFilename">The file name of the picture to process.</param>
        /// <param name="pictureMetadataList">The list with metadata to add to.</param>
        /// <remarks>If the metadata cannot be read successfully it is skipped and a warning will be written
        /// to the trace listeners.</remarks>
        private void AddSyncMetadata(string pictureFilename, List<LocalPictureMetadata> pictureMetadataList)
        {
            try
            {
                pictureMetadataList.Add(GetSyncMetadata(pictureFilename));
            }
            catch (LocalMetadataException ex)
            {
                Trace.TraceError(ex.ToString());
                Trace.TraceWarning("Skipping picture '{0}' because of unreadable metadata.", ex.FileName);
            }
        }
        
        /// <summary>
        /// Reports the progress to a callback and checks for cancellation.
        /// </summary>
        /// <param name="progressCallback">A callback to receive progress information. Is allowed to be null.</param>
        /// <param name="currentPictureNumber">The sequence number of the picture that was just processed.</param>
        /// <param name="numberOfPicturesToProcess">The total number of pictures to process.</param>
        /// <returns><c>True</c> if the operation should be cancelled, <c>false</c> otherwise.</returns>
        private bool ReportProgressAndCheckCancel(EventHandler<PictureProgressEventArgs> progressCallback, int currentPictureNumber, int numberOfPicturesToProcess)
        {
            if (progressCallback != null)
            {
                PictureProgressEventArgs e = new PictureProgressEventArgs(currentPictureNumber, numberOfPicturesToProcess);
                progressCallback(this, e);
                if (e.CancelProcess)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Updates the metadata in local pictures.
        /// </summary>
        /// <param name="updatedLocalPictures">The local pictures that need updating.</param>
        /// <param name="progressCallback">A callback to receive progress information. Is allowed to be null.</param>
        public void UpdatePictureMetadata(IList<LocalPictureMetadata> updatedLocalPictures, EventHandler<PictureProgressEventArgs> progressCallback)
        {
            if (_simulateUpdatingMetadata)
            {
                Trace.TraceInformation(Properties.Messages.SimulatingUpdatingLocalPictures);
            }

            int currentPhotoNumber = 1;
            foreach (LocalPictureMetadata pictureMetadata in updatedLocalPictures)
            {
                try
                {
                    UpdatePictureMetadata(pictureMetadata);
                }
                catch (Exception ex)
                {
                    Trace.TraceError("Failed to update picture '{0}': "
                       + System.Environment.NewLine + "{1}", pictureMetadata.FilePath, ex.ToString());
                }

                if (progressCallback != null)
                {
                    PictureProgressEventArgs e = new PictureProgressEventArgs(currentPhotoNumber, updatedLocalPictures.Count);
                    progressCallback(this, e);
                    if (e.CancelProcess)
                    {
                        return;
                    }
                }

                currentPhotoNumber++;
            }
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Sets the bitmap metadata using WIC.
        /// </summary>
        /// <param name="bitmapMetadata">The bitmap metadata to set.</param>
        /// <param name="metadata">The internal metadata structure with the data to use.</param>
        private void SetMetadata(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            UpdateTitle(bitmapMetadata, metadata);

            UpdateDescription(bitmapMetadata, metadata);

            UpdateFlickrId(bitmapMetadata, metadata);

            UpdateTags(bitmapMetadata, metadata);

            UpdateAuthor(bitmapMetadata, metadata);

            UpdateGpsCoordinates(bitmapMetadata, metadata);

            UpdateDateTaken(bitmapMetadata, metadata);
        }

        /// <summary>
        /// Updates the date taken in the metadata if it should be changed.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        /// <param name="metadata">The local metadata to set.</param>
        private void UpdateDateTaken(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            if (metadata.DateTakenChanged)
            {
                if (!_simulateUpdatingMetadata)
                {
                    bitmapMetadata.DateTaken = metadata.DateTaken.ToString("G", CultureInfo.InvariantCulture);
                }

                Trace.TraceInformation("Updated date taken for local picture with id {0} from '{1}' to '{2}'.",
                    metadata.Id, metadata.DateTakenOriginal, metadata.DateTaken);
            }
        }

        /// <summary>
        /// Updates the GPS coordinates in the metadata if they should be changed.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        /// <param name="metadata">The local metadata to set.</param>
        private void UpdateGpsCoordinates(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            if (metadata.GpsLatitudeChanged || metadata.GpsLongitudeChanged)
            {
                if (metadata.GpsLatitude.HasValue && metadata.GpsLongitude.HasValue)
                {
                    SetGpsCoordinates(bitmapMetadata, metadata);

                    Trace.TraceInformation("Updated geolocation for  picture with id {0} from ({1} , {2}) to ({3} , {4}).",
                       metadata.Id, metadata.GpsLatitudeOriginal.ToString(), metadata.GpsLongitudeOriginal.ToString(), metadata.GpsLatitude.Value, metadata.GpsLongitude.Value);
                }
                else
                {
                    RemoveGpsCoordinates(bitmapMetadata);

                    Trace.TraceInformation("Removed geolocation for local picture with id {0}.", metadata.Id);
                }
            }
        }

        /// <summary>
        /// Sets the GPS coordinates in the metadata.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        /// <param name="metadata">The local metadata to set.</param>
        private void SetGpsCoordinates(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            if (!_simulateUpdatingMetadata)
            {
                bitmapMetadata.SetQuery(_XmpLatitudeQuery, GpsCoordinatesHelper.EncodeXmpCoordinate(metadata.GpsLatitude.Value));
                bitmapMetadata.SetQuery(_XmpLongitudeQuery, GpsCoordinatesHelper.EncodeXmpCoordinate(metadata.GpsLongitude.Value));

                bitmapMetadata.SetQuery(_GpsVersionQuery, GpsVersion.Normal.Bytes);

                bitmapMetadata.SetQuery(_LatitudeQuery, GpsCoordinatesHelper.EncodeExifCoordinate(metadata.GpsLatitude.Value));
                bitmapMetadata.SetQuery(_LongitudeQuery, GpsCoordinatesHelper.EncodeExifCoordinate(metadata.GpsLongitude.Value));

                char northOrSouth = (metadata.GpsLatitude.Value >= 0) ? 'N' : 'S';
                bitmapMetadata.SetQuery(_NorthOrSouthQuery, northOrSouth);

                char eastOrWest = (metadata.GpsLongitude.Value >= 0) ? 'E' : 'W';
                bitmapMetadata.SetQuery(_EastOrWestQuery, eastOrWest);
            }
        }

        /// <summary>
        /// Removes the GPS coordinates from the WIC metadata.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        private void RemoveGpsCoordinates(BitmapMetadata bitmapMetadata)
        {
            if (!_simulateUpdatingMetadata)
            {
                bitmapMetadata.RemoveQuery(_LatitudeQuery);
                bitmapMetadata.RemoveQuery(_LongitudeQuery);
                bitmapMetadata.RemoveQuery(_NorthOrSouthQuery);
                bitmapMetadata.RemoveQuery(_EastOrWestQuery);
                bitmapMetadata.RemoveQuery(_XmpLatitudeQuery);
                bitmapMetadata.RemoveQuery(_XmpLongitudeQuery);
            }
        }

        /// <summary>
        /// Updates the author in the metadata if it should be changed.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        /// <param name="metadata">The local metadata to set.</param>
        private void UpdateAuthor(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            if (metadata.AuthorChanged)
            {
                if (!_simulateUpdatingMetadata)
                {
                    ReadOnlyCollection<string> author = new ReadOnlyCollection<string>(new string[] { metadata.Author });
                    bitmapMetadata.Author = author;
                }

                Trace.TraceInformation("Updated author for local picture with id {0} from '{1}' to '{2}'.",
                   metadata.Id, metadata.AuthorOriginal, metadata.Author);
            }
        }

        /// <summary>
        /// Updates the tags in the metadata if they should be changed.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        /// <param name="metadata">The local metadata to set.</param>
        private void UpdateTags(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            if (metadata.TagsChanged)
            {
                if (!_simulateUpdatingMetadata)
                {
                    ReadOnlyCollection<string> keywords = new ReadOnlyCollection<string>(metadata.Tags);
                    bitmapMetadata.Keywords = keywords;
                }

                Trace.TraceInformation("Updated tags for local picture with id {0} from '{1}' to '{2}'.",
                   metadata.Id, String.Join(";", metadata.TagsOriginal), String.Join(";", metadata.Tags));
            }
        }

        /// <summary>
        /// Updates the description in the metadata if it should be changed.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        /// <param name="metadata">The local metadata to set.</param>
        private void UpdateDescription(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            if (metadata.DescriptionChanged)
            {
                if (!_simulateUpdatingMetadata)
                {
                    if (!Settings.Default.UseAlternateLocalMetadataMapping)
                    {
                        bitmapMetadata.Comment = metadata.Description;
                    }
                    else
                    {
                        TryUpdateAlternateDescription(bitmapMetadata, _AlternateDescriptionQuery1, metadata);
                        TryUpdateAlternateDescription(bitmapMetadata, _AlternateDescriptionQuery2, metadata);
                        TryUpdateAlternateDescription(bitmapMetadata, _AlternateDescriptionQuery3, metadata);
                    }
                }

                Trace.TraceInformation("Updated description for local picture with id {0} from '{1}' to '{2}'.",
                   metadata.Id, metadata.DescriptionOriginal, metadata.Description);
            }
        }

        private void TryUpdateAlternateDescription(BitmapMetadata bitmapMetadata, string alternateDescriptionQuery, LocalPictureMetadata metadata)
        {
            try
            {
                bitmapMetadata.SetQuery(alternateDescriptionQuery, metadata.Description);
            }
            catch (Exception ex)
            {
                Trace.TraceWarning("Failed to update alternate description for local picture with id {0} from '{1}' to '{2}' due to an exception. Alternate description query: {3}. Exception message: {4}",
                   metadata.Id, metadata.DescriptionOriginal, metadata.Description, alternateDescriptionQuery, ex.Message);
            }
        }


        /// <summary>
        /// Updates the title in the metadata if it should be changed.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        /// <param name="metadata">The local metadata to set.</param>
        private void UpdateTitle(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            if (metadata.TitleChanged)
            {
                if (!_simulateUpdatingMetadata)
                {
                    if (!Settings.Default.UseAlternateLocalMetadataMapping)
                    {
                        bitmapMetadata.Title = metadata.Title;
                    }
                    else
                    {
                        TryUpdateAlternateTitle(bitmapMetadata, _AlternateTitleQuery1, metadata);
                        TryUpdateAlternateTitle(bitmapMetadata, _AlternateTitleQuery2, metadata);
                    }
                }

                Trace.TraceInformation("Updated title for local picture with id {0} from '{1}' to '{2}'.",
                   metadata.Id, metadata.TitleOriginal, metadata.Title);
            }
        }

        private void TryUpdateAlternateTitle(BitmapMetadata bitmapMetadata, string alternateTitleQuery, LocalPictureMetadata metadata)
        {
            try
            {
                bitmapMetadata.SetQuery(alternateTitleQuery,  metadata.Title);
            }
            catch (Exception ex)
            {
                Trace.TraceWarning("Failed to update alternate title for local picture with id {0} from '{1}' to '{2}' due to an exception. Alternate title query: {3}. Exception message: {4}",
                   metadata.Id, metadata.TitleOriginal, metadata.Title, alternateTitleQuery, ex.Message);
            }
        }

        /// <summary>
        /// Updates the Flickr id in the metadata if it has changed or needs updating. Removes it if it should be removed.
        /// </summary>
        /// <param name="bitmapMetadata">The WIC instance to update the metadata in the local picture.</param>
        /// <param name="metadata">The local metadata to set.</param>
        /// <remarks>This method will change the old style Flickr id embedded in metadata to the current format.</remarks>
        private void UpdateFlickrId(BitmapMetadata bitmapMetadata, LocalPictureMetadata metadata)
        {
            switch (this.FlickrIdBehavior)
            {
                case FlickrIdBehavior.AddOrUpdate:
                    string existingUniqueImageId = (string)bitmapMetadata.GetQuery(_UniqueImageIdQuery);
                    bool containsOldFormatFlickrId = FlickrIdHelper.ContainsOldFormatFlickrId(existingUniqueImageId);
                    bool containsCurrentFormatFlickrId = FlickrIdHelper.ContainsFlickrId(existingUniqueImageId);

                    if (metadata.FlickrIdChanged || containsOldFormatFlickrId)
                    {
                        bool alreadyHasOtherUniqueImageId =
                            !String.IsNullOrEmpty(existingUniqueImageId)
                            && !containsOldFormatFlickrId
                            && !containsCurrentFormatFlickrId;

                        if (!alreadyHasOtherUniqueImageId)
                        {
                            StoreFlickrIdInBitmapMetadata(metadata, bitmapMetadata);
                        }
                        else
                        {
                            Trace.TraceInformation("Not setting Flickr id for local picture with id {0} to '{1}' because it already contains another unique image id '{2}'.",
                               metadata.Id, metadata.FlickrId, existingUniqueImageId);
                        }
                    }
                    break;
                case FlickrIdBehavior.Remove:
                    if (!String.IsNullOrEmpty(metadata.FlickrIdOriginal))
                    {
                        if (!_simulateUpdatingMetadata)
                        {
                            bitmapMetadata.RemoveQuery(_UniqueImageIdQuery);
                        }

                        Trace.TraceInformation("Removed Flickr id from local picture with id {0}.",
                           metadata.Id);
                    }
                    break;
                case FlickrIdBehavior.OverwriteOrUpdate:
                    if (metadata.FlickrIdChanged)
                    {
                        StoreFlickrIdInBitmapMetadata(metadata, bitmapMetadata);
                    }
                    break;

                case FlickrIdBehavior.DoNothing:
                    break;
                default:
                    throw new InvalidOperationException("Unsupported FlickrIdBehavior " + this.FlickrIdBehavior.ToString() + ".");
            }
        }

        /// <summary>
        /// Stores the Flickr id in the bitmap metadata for the local picture using WIC. 
        /// </summary>
        /// <param name="metadata">The <see cref="LocalPictureMetadata"/> instance containing the Flickr id.</param>
        /// <param name="bitmapMetadata">The WIC metadata to update.</param>
        private void StoreFlickrIdInBitmapMetadata(LocalPictureMetadata metadata, BitmapMetadata bitmapMetadata)
        {
            if (!_simulateUpdatingMetadata)
            {
                string metadataValue = FlickrIdHelper.GetFlickrIdForMetadata(metadata.FlickrId);

                bitmapMetadata.SetQuery(_UniqueImageIdQuery, metadataValue);
            }

            Trace.TraceInformation("Updated Flickr id for local picture with id {0} from '{1}' to '{2}'.",
               metadata.Id, metadata.FlickrIdOriginal, metadata.FlickrId);
        }

        /// <summary>
        /// Updates the metadata for a local picture.
        /// </summary>
        /// <param name="metadata">The metadata.</param>
        /// <remarks>This method may be called on any thread.</remarks>
        private void UpdatePictureMetadata(LocalPictureMetadata metadata)
        {
            if (!metadata.Changed)
            {
                return;
            }

            // Invoke the updating of metadata using WIC through the dispatcher of the WIC STA.
            // WIC throws exceptions when trying to update metadata from a non-STA thread. 
            _wicStaCommunication.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Background,
               (SendOrPostCallback)delegate(object o)
               {
                   UpdatePictureMetadataUsingWic((LocalPictureMetadata)o);
               }, metadata);
        }

        /// <summary>
        /// Updates the metadata for a local picture using WIC.
        /// </summary>
        /// <param name="metadata">The metadata.</param>
        /// <remarks>Uses the <see cref="InPlaceMetadataWriter"/> class and other WIC classes to update metadata.
        /// It should only be called on an STA thread.</remarks>
        private void UpdatePictureMetadataUsingWic(LocalPictureMetadata metadata)
        {
            bool updateSucceeded = TryUpdatePictureMetadataUsingWicUsingInPlaceMetadataWriter(metadata);

            if (updateSucceeded)
            {
                Trace.TraceInformation("InPlaceMetadata update succeeded for local picture with id {0}.", metadata.Id);
            }
            else
            {
                Trace.TraceInformation("InPlaceMetadata update failed for local picture with id {0}. Will retry using copy.", metadata.Id);

                bool canRetryWithoutPadding;

                updateSucceeded = UpdatePictureMetadataUsingWicWithCopy(metadata, addPadding: true, canRetryWithoutPadding: out canRetryWithoutPadding);

                if (!updateSucceeded && canRetryWithoutPadding)
                {
                    updateSucceeded = UpdatePictureMetadataUsingWicWithCopy(metadata, addPadding: false, canRetryWithoutPadding: out canRetryWithoutPadding);
                }
            }
        }

        /// <summary>
        /// Changes the extension in a filename with path and returns it.
        /// </summary>
        /// <param name="filePath">The filename with path.</param>
        /// <param name="newExtension">The new extension.</param>
        /// <returns>The filename with path with the new extension.</returns>
        private string ChangeExtension(string filePath, string newExtension)
        {
            int dotIndex = filePath.LastIndexOf('.');

            if (dotIndex == -1)
            {
                return filePath;
            }
            else
            {
                return filePath.Substring(0, dotIndex + 1) + newExtension;
            }
        }

        /// <summary>
        /// Tries to update the metadata of a local picture using the <see cref="InPlaceBitmapMetadataWriter"/>.
        /// </summary>
        /// <param name="metadata">The metadata to write.</param>
        /// <returns><c>True</c> when successful, <c>false</c> otherwise.</returns>
        /// <remarks>This method should only be called on an STA thread.</remarks>
        private bool TryUpdatePictureMetadataUsingWicUsingInPlaceMetadataWriter(LocalPictureMetadata metadata)
        {
            try
            {
                using (Stream savedFile = File.Open(metadata.FilePath, FileMode.Open, FileAccess.ReadWrite))
                {
                    BitmapDecoder output = BitmapDecoder.Create(savedFile, BitmapCreateOptions.None, BitmapCacheOption.Default);

                    InPlaceBitmapMetadataWriter bitmapMetadata = output.Frames[0].CreateInPlaceBitmapMetadataWriter();

                    SetMetadata(bitmapMetadata, metadata);

                    if (_simulateUpdatingMetadata || bitmapMetadata.TrySave())
                    {
                        return true;
                    }
                    else
                    {
                        return false;
                    }
                }
            }
            catch (IOException ex)
            {
                Trace.TraceError("Failed to open file '{0}' for writing. Exception details follow:"
                   + System.Environment.NewLine + "{1}", metadata.FilePath, ex.ToString());

                return false;
            }
            catch (UnauthorizedAccessException ex)
            {
                Trace.TraceError("Failed to open file '{0}' for writing. Exception details follow:"
                   + System.Environment.NewLine + "{1}", metadata.FilePath, ex.ToString());

                return false;
            }
            catch (ArgumentException ex)
            {
                Trace.TraceError("Failed to update metadata in file '{0}'. Exception details follow:"
                   + System.Environment.NewLine + "{1}", metadata.FilePath, ex.ToString());

                return false;
            }
        }

        /// <summary>
        /// Updates the metadata of a local picture by copying the image using a lossless transcode operation
        /// to a new file.
        /// </summary>
        /// <param name="metadata">The metadata to write.</param>
        /// <param name="addPadding">If padding should be added.</param>
        /// <param name="canRetryWithoutPadding">Output parameter. <c>true</c> if the operation can be retried without padding if it failed, <c>false</c> if it cannot/should not be retried.</param>
        /// <returns><c>true</c> if the update was successful, <c>false</c> if it failed.</returns>
        /// <remarks>This method should only be called on an STA thread.</remarks>
        private bool UpdatePictureMetadataUsingWicWithCopy(LocalPictureMetadata metadata, bool addPadding, out bool canRetryWithoutPadding)
        {
            canRetryWithoutPadding = false;
            string originalFileName = metadata.FilePath;
            string outputFileName = ChangeExtension(originalFileName, "out");

            try
            {
                bool success;

                using (Stream originalFile = new FileStream(originalFileName, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    success = WriteCopyOfPictureUsingWic(originalFile, metadata, outputFileName, addPadding);
                }

                if (success)
                {
                    File.Delete(originalFileName);
                    File.Move(outputFileName, originalFileName);
                }

                return true;
            }
            catch (IOException ex)
            {
                HandleUpdatePictureMetadataUsingWicWithCloneException(ex, originalFileName, outputFileName, addPadding: true, addExceptionDetails: true);
            }
            catch (UnauthorizedAccessException ex)
            {
                HandleUpdatePictureMetadataUsingWicWithCloneException(ex, originalFileName, outputFileName, addPadding: true, addExceptionDetails: true);
            }
            catch (ArgumentException ex)
            {
                HandleUpdatePictureMetadataUsingWicWithCloneException(ex, originalFileName, outputFileName, addPadding: true, addExceptionDetails: true);
            }
            catch (FileFormatException ex)
            {
                bool addExceptionDetails = true;
                COMException comException = ex.InnerException as COMException;
                if (COMExceptionHasErrorCode(comException, _PaddingCOMExceptionErrorCode))
                {
                    canRetryWithoutPadding = true;
                    addExceptionDetails = false;
                }

                HandleUpdatePictureMetadataUsingWicWithCloneException(ex, originalFileName, outputFileName, addPadding: true, addExceptionDetails: addExceptionDetails);
            }

            return false;
        }

        /// <summary>
        /// Checks if the specified <see cref="COMException"/> has the specified error code.
        /// </summary>
        /// <param name="ex">The <see cref="COMException"/>. Is allowed to be <c>null</c>.</param>
        /// <param name="errorCode">The error code to compare against.</param>
        /// <returns><c>true</c> if the exception has the specified error code or <paramref name="ex"/> is <c>null</c>, <c>false</c> otherwise.</returns>
        private bool COMExceptionHasErrorCode(COMException ex, uint errorCode)
        {
            if (ex == null)
            {
                return false;
            }
            return unchecked( (uint) ex.ErrorCode == errorCode );
        }

        /// <summary>
        /// Handles an exception that occurred when cloning the original picture file.
        /// </summary>
        /// <param name="ex">The exception that occurred.</param>
        /// <param name="originalFileName">The filename of the original file.</param>
        /// <param name="outputFileName">The filename used for the clone.</param>
        /// <param name="addPadding">Whether the clone operation tried to add padding to the metadata.</param>
        /// <param name="addExceptionDetails">Whether exception details should be added to the trace message.</param>
        private void HandleUpdatePictureMetadataUsingWicWithCloneException(Exception ex, string originalFileName, string outputFileName, bool addPadding, bool addExceptionDetails)
        {
            string errorMessage = String.Format(
                CultureInfo.InvariantCulture,
                "Failed to clone file '{0}' with updated metadata and with addPadding set to {1}.",
                originalFileName,
                addPadding);

            if (addExceptionDetails)
            {
                errorMessage = String.Format(
                    CultureInfo.InvariantCulture,
                    " Exception details follow:" + System.Environment.NewLine + "{0}",
                    ex.ToString());
            }

            Trace.TraceError(errorMessage);

            // Clean up temporary file.
            try
            {
                File.Delete(outputFileName);
            }
            catch (IOException ex2)
            {
                Trace.TraceError("Failed to delete temporary file '{0}'. Exception details follow:"
                   + System.Environment.NewLine + "{1}", outputFileName, ex2.ToString());
            }
        }

        /// <summary>
        /// Copies a picture file stream and writes updated metadata in the copy.
        /// </summary>
        /// <param name="originalFile">A <see cref="Stream"/> for the original file that
        /// should be copied.</param>
        /// <param name="metadata">The metadata to write.</param>
        /// <param name="outputFileName">The name of the output file.</param>
        /// <param name="addPadding">If padding should be added to the metadata.</param>
        /// <returns><c>True</c> if the copy was written successfully, <c>false</c> otherwise.</returns>
        /// <remarks>This method should only be called on an STA thread.</remarks>
        private bool WriteCopyOfPictureUsingWic(Stream originalFile, LocalPictureMetadata metadata, string outputFileName, bool addPadding)
        {
            // Notice the BitmapCreateOptions and BitmapCacheOption. Using these options in the manner here
            // will inform the JPEG decoder and encoder that we're doing a lossless transcode operation. If the
            // encoder is anything but a JPEG encoder, then this no longer is a lossless operation.
            // ( Details: Basically BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile 
            //   tell the decoder to use the original image bits and BitmapCacheOption.None tells the decoder to wait 
            //   with decoding. So, at the time of encoding the JPEG encoder understands that the input was a JPEG
            //   and just copies over the image bits without decompressing and recompressing them. Hence, this is a
            //   lossless operation. )
            BitmapCreateOptions createOptions = BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile;
            BitmapDecoder original = BitmapDecoder.Create(originalFile, createOptions, BitmapCacheOption.None);

            if (!original.CodecInfo.FileExtensions.Contains("jpg"))
            {
                Trace.TraceInformation("Update failed for local picture with id {0} because it is not a JPEG file.", metadata.Id);
                return false;
            }

            JpegBitmapEncoder output = new JpegBitmapEncoder();

            BitmapFrame originalFrame = original.Frames[0];

            if (originalFrame == null)
            {
                throw new LocalMetadataException("Cannot create copy of file without bitmap frames.", metadata.FilePath);
            }

            BitmapFrame newFrame = GetClonedBitmapFrameWithUpdatedMetadata(originalFrame, metadata, addPadding);

            output.Frames.Add(newFrame);

            using (Stream outputFile = File.Open(outputFileName, FileMode.Create, FileAccess.ReadWrite))
            {
                output.Save(outputFile);
            }

            return true;
        }

        /// <summary>
        /// Creates and returns a clone of the provided <see cref="BitmapFrame"/> with updated metadata that is optionally padded.
        /// </summary>
        /// <param name="originalFrame">The original <see cref="BitmapFrame"/>.</param>
        /// <param name="metadata">The metadata that should be set.</param>
        /// <param name="addPadding">If padding should be added to the metadata.</param>
        /// <returns>A new bitmapframe that is a clone of the original frame, but with updated and optionally padded metadata.</returns>
        /// <remarks>This method should only be called on an STA thread.</remarks>
        private BitmapFrame GetClonedBitmapFrameWithUpdatedMetadata(BitmapFrame originalFrame, LocalPictureMetadata metadata, bool addPadding)
        {
            // If you're just interested in doing a lossless transcode without adding metadata, just do this:
            //output.Frames = original.Frames;
            // If you want to add metadata to the image using the InPlaceBitmapMetadataWriter, first add padding:
            // Your gut feel may want you to do something like:
            //     output.Frames = original.Frames;
            //     BitmapMetadata metadata = output.Frames[0].Metadata as BitmapMetadata;
            //     if (metadata != null)
            //     {
            //         metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", paddingAmount);
            //     }
            // However, the BitmapMetadata object is frozen. So, you need to clone the BitmapMetadata and then
            // set the padding on it. Lastly, you need to create a "new" frame with the updated metadata.

            ImageMetadata originalBitmapMetadata = originalFrame.Metadata;
            BitmapMetadata clonedBitmapMetadata;
            if (originalBitmapMetadata != null)
            {
                clonedBitmapMetadata = originalFrame.Metadata.Clone() as BitmapMetadata;
            }
            else
            {
                clonedBitmapMetadata = new BitmapMetadata("jpeg");
            }

            if (addPadding)
            {
                // Of the metadata handlers that we ship in WIC, padding can only exist in IFD, EXIF, and XMP.
                // Third parties implementing their own metadata handler may wish to support IWICFastMetadataEncoder
                // and hence support padding as well.
                clonedBitmapMetadata.SetQuery("/app1/ifd/PaddingSchema:Padding", _MetadataPaddingInBytes);
                clonedBitmapMetadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", _MetadataPaddingInBytes);
                clonedBitmapMetadata.SetQuery("/xmp/PaddingSchema:Padding", _MetadataPaddingInBytes);
            }

            SetMetadata(clonedBitmapMetadata, metadata);

            // Create a new frame identical to the one from the original image, except the metadata will have padding.
            // Essentially we want to keep this as close as possible to:
            //     output.Frames = original.Frames;
            BitmapFrame clonedFrame = BitmapFrame.Create(originalFrame, originalFrame.Thumbnail, clonedBitmapMetadata, originalFrame.ColorContexts);

            return clonedFrame;
        }

        /// <summary>
        /// Constructs a <see cref="LocalMetadataException"/> object with an error message
        /// that reading the metadata for <paramref name="pictureFileName"/> failed.
        /// </summary>
        /// <param name="pictureFileName">The name of the file for which reading metadata failed.</param>
        /// <param name="innerException">The inner exception.</param>
        /// <returns>A <see cref="LocalMetadataException"/> object with an error message
        /// that reading the metadata for <paramref name="pictureFileName"/> failed.</returns>
        private static LocalMetadataException GetReadLocalMetadataException(string pictureFileName, Exception innerException)
        {
            string errorMessage = String.Format(CultureInfo.CurrentCulture, Messages.ReadingLocalPictureFailed, pictureFileName);

            return new LocalMetadataException(errorMessage, pictureFileName, innerException);
        }

        /// <summary>
        /// Returns <see cref="String.Empty"/> if <paramref name="metadataItem"/> only contains whitespace or
        /// if it is <c>null</c>.
        /// </summary>
        /// <param name="metadataItem">The metadata string to fix if it should be "empty".</param>
        /// <returns><see cref="String.Empty"/> if <paramref name="metadataItem"/> only contains whitespace or
        /// if it is <c>null</c>.</returns>
        private static string FixEmptyMetadata(string metadataItem)
        {
            if (String.IsNullOrEmpty(metadataItem) || metadataItem.Trim().Length == 0)
            {
                return "";
            }
            else
            {
                return metadataItem;
            }
        }

        /// <summary>
        /// Starts a new STA thread for accessing WIC classes to update metadata.
        /// </summary>
        /// <remarks>WIC v1 only allows metadata updates from an STA apartment. Normally this would be from the UI thread,
        /// but the downside of using this STA is that the app becomes unresponsive if the update takes some time.
        /// It is better to create a separate STA for this.</remarks>
        private void StartWicStaThread()
        {
            _wicStaCommunication = new WicStaCommunication();

            Thread thread = new Thread(RunWicStaThread);

            thread.IsBackground = true;
            thread.Name = "WicStaThread";
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start(_wicStaCommunication);

            // Block this thread until the new thread has spun up, initialized and has signaled us.
            _wicStaCommunication.Signal.WaitOne();

            // Now the dispatcher can be safely accessed.
        }

        /// <summary>
        /// The thread start method for the STA thread for updating WIC metadata. Starts its own
        /// <see cref="Dispatcher"/> loop.
        /// </summary>
        /// <param name="arg">The cross-thread communication object. Should be of type
        /// <see cref="WicStaCommunication"/>.</param>
        private void RunWicStaThread(object arg)
        {
            WicStaCommunication wicStaCommunication = (WicStaCommunication)arg;

            if (wicStaCommunication == null)
            {
                throw new ArgumentException("Invalid thread start parameter.", "arg");
            }

            wicStaCommunication.Dispatcher = Dispatcher.CurrentDispatcher;

            // Signal any waiting thread that we now have our own dispatcher.
            wicStaCommunication.Signal.Set();

            // Run the dispatcher loop, this call will block.
            Dispatcher.Run();
        }
        #endregion
    }
}
